/*
 * Copyright (C) 2013 Manoel Afonso Filho
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package agimagem.populacao;

import agimagem.context.AG;
import agimagem.util.AGChave;
import java.util.Arrays;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
 * Especificação do indivíduo do algoritmo genético. Além da sequência de bytes
 * representando o texto que está sendo cifrado, este objeto armazena também
 * todas as chaves usadas para a geração da sequência binária atual bem como o
 * seu fitness.
 * @author Manoel Afonso Filho
 */
public class Individuo implements Comparable<Individuo> {

    private static Logger log = LogManager.getLogger(Individuo.class.getName());

    /**
     * Todas as chaves utilizadas para se chegar a sequência binária atual.
     */
    private AGChave chaves[] = new AGChave[AG.getNumGeracoes()];

    /**
     * Texto cifrado atual deste indivíduo.
     */
    private byte[] ciphertext;

    /**
     * Valor do fitness deste indivíduo.
     */
    private double fitness;

    /**
     * Quantas gerações este indivíduo passou no AG.
     */
    private int idade;

    /**
     * Cria um indivíduo com ciphertext nulo e fitness igual a 0.
     */
    public Individuo() {
        this.ciphertext = null;
        this.fitness = 0;
    }

    /**
     * Cria um indivíduo com o ciphertext dado e fitness igual a 0.
     * @param ct Sequência binária deste indivíduo.
     */
    public Individuo(byte[] ct) {
        byte[] novo = new byte[ct.length];
        System.arraycopy(ct, 0, novo, 0, novo.length);
        this.ciphertext = novo;
        this.fitness = 0;
    }

    /**
     * @return Uma cópia do ciphertext.
     */
    public byte[] getCiphertext() {
        byte[] novo = new byte[ciphertext.length];
        System.arraycopy(ciphertext, 0, novo, 0, novo.length);
        return novo;
    }

    /**
     * @param ct Ciphertext
     */
    public void setCiphertext(byte[] ct) {
        System.arraycopy(ct, 0, this.ciphertext, 0, this.ciphertext.length);
    }

    /**
     * @return O valor do fitness.
     */
    public double getFitness() {
        return fitness;
    }

    /**
     * @param fitness the fitness to set
     */
    public void setFitness(double fitness) {
        this.fitness = fitness;
    }

    /**
     * @return the idade
     */
    public int getIdade() {
        return idade;
    }

    /**
     * @param idade the idade to set
     */
    public void setIdade(int idade) {
        this.idade = idade;
    }

    /**
     * @return Uma referência ao objeto {@link AGChave} sendo usado na geração
     * atual.
     * @see {@link Individuo#getChaveDaGeracao(int) Obter chave de uma geração específica}.
     */
    public AGChave getChaveDaGeracaoAtual() {
        return getChaveDaGeracao(AG.getGeracaoAtual());
    }

    /**
     * Obtém a chave de uma determinda geração do algoritmo. Se ainda não houver
     * uma chave para a geração requerida, então uma nova é criada.
     * @param geracao Número da geração
     * @return Referência ao objeto Chave da geração dada.
     */
    public AGChave getChaveDaGeracao(int geracao) {
        if (chaves[geracao] == null) {
            chaves[geracao] = new AGChave();
        }
        return chaves[geracao];
    }

    //Pode retornar null
    public AGChave getChaveDaGeracaoNull(int geracao){
        return chaves[geracao];
    }

    /**
     * @return Uma cópia do array de chaves.
     */
    public AGChave[] getChaves() {
        AGChave[] novo = new AGChave[chaves.length];
        try {
            if (chaves != null) {
                for (int i = 0; i < chaves.length; i++) {
                    if (chaves[i] != null) {
                        novo[i] = (AGChave) chaves[i].clone();
                    }
                }
            }
        } catch (CloneNotSupportedException ex) {
            log.error("Erro na clonagem de Individuo:\n", ex);
        }

        return novo;
    }

    /**
     * @param chaves the chaves to set
     */
    public void setChaves(AGChave[] chaves) {
        System.arraycopy(chaves, 0, this.chaves, 0, this.chaves.length);
    }

    /**
     * Ordena em ordem crescente de acordo com o valor do fitness.
     * @param o Individuo para ser comparado com este.
     * @return 1 se este indivíduo tiver fitness maior, 0 se os dois possuírem
     * o mesmo valor do fitness, e -1 caso o outro indivíduo tenha fitness maior.
     */
    @Override
    public int compareTo(Individuo o) {
        if (this.getFitness() > o.getFitness()) {
            return 1;
        } else if (this.getFitness() == o.getFitness()) {
            return 0;
        } else {
            return -1;
        }
    }

    @Override
    public String toString() {
        return "f = " + this.getFitness();
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Individuo) {
            Individuo ind = (Individuo) obj;
            if (this.fitness == ind.getFitness()) {
                return true;
            } else {
                return false;
            }
        } else {
            throw new IllegalArgumentException("obj deve ser do tipo Individuo");
        }
    }

    @Override
    public int hashCode() {
        int hash = 3;
        hash = 41 * hash + Arrays.deepHashCode(this.chaves);
        hash = 41 * hash + Arrays.hashCode(this.ciphertext);
        hash = 41 * hash + (int) (Double.doubleToLongBits(this.fitness) ^ (Double.doubleToLongBits(this.fitness) >>> 32));
        hash = 41 * hash + this.idade;
        return hash;
    }

    /**
     * Efetua deep cloning deste objeto.
     * @return Um cópia deste objeto Indivíduo.
     * @throws CloneNotSupportedException
     */
    @Override
    public Object clone() throws CloneNotSupportedException {
        Individuo novoInd = new Individuo(this.getCiphertext());
//        novoInd.setChaves(this.getChaves());
        novoInd.setChaves(this.chaves);
        novoInd.setFitness(this.fitness);
        novoInd.setIdade(this.idade);
        return novoInd;
    }
}
